﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml.Serialization;

namespace PrecedentLib
{
    /// <summary>
    ///     PrecedentAggregator by Hermann Ebbinghaus formula
    /// </summary>
    /// <typeparam name="T">Type to store as precedent</typeparam>
    public class PrecedentContainer<T> : INotifyPropertyChanged
    {
        private int capacity;
        private List<PrecedentAggregator> elements;
        private IEqualityComparer<T> equalityComparer;
        private TimeSpan halfLife;

        /// <summary>
        ///     Constructor
        /// </summary>
        /// <param name="capacity">Max number of stored elements</param>
        /// <param name="halfLife">
        ///     The period (halfLife) of remembrance
        ///     Rule: 2 precedents after the period costs the same as 1 at the moment
        ///     and 4 precedents after 2 periods of halflife cost as 1 at the moment
        /// </param>
        public PrecedentContainer(int capacity, TimeSpan halfLife) : this(capacity, halfLife, null)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        /// <param name="capacity">Max number of stored elements</param>
        /// <param name="halfLife">The period (halfLife) of remembrance</param>
        /// <param name="equalityComparer">Equality Comparer</param>
        public PrecedentContainer(int capacity, TimeSpan halfLife, IEqualityComparer<T> equalityComparer)
        {
            Capacity = capacity;
            Elements = new List<PrecedentAggregator>(capacity);
            HalfLife = halfLife;
            if (equalityComparer == null)
            {
                if (EqualityComparer == null)
                {
                    EqualityComparer = EqualityComparer<T>.Default;
                }
            }
            else
            {
                EqualityComparer = equalityComparer;
            }
        }

        public List<PrecedentAggregator> Elements
        {
            get { return elements; }
            set
            {
                elements = value;
                NotifyPropertyChange("Elements");
            }
        }

        public int Capacity
        {
            get { return capacity; }
            set
            {
                capacity = value;
                NotifyPropertyChange("Capacity");
            }
        }

        public TimeSpan HalfLife
        {
            get { return halfLife; }
            set
            {
                halfLife = value;
                NotifyPropertyChange("HalfLife");
            }
        }

        public IEqualityComparer<T> EqualityComparer
        {
            get { return equalityComparer; }
            set
            {
                equalityComparer = value;
                NotifyPropertyChange("EqualityComparer");
            }
        }

        public void AddPrecedent(T precedentValue)
        {
            // Get precedent occurence date (now)
            var occurrenceDate = DateTime.Now;
            PrecedentAggregator precedentAggregator = null;
            foreach (var aggregator in Elements)
            {
                if ((EqualityComparer.Equals(aggregator.PrecedentValue, precedentValue))
                    && (EqualityComparer.Equals(precedentValue, aggregator.PrecedentValue)))
                {
                    precedentAggregator = aggregator;
                    break;
                }
            }
            if (precedentAggregator == null)
            {
                if (Elements.Count >= Capacity)
                {
                    ForgetElement();
                }
                precedentAggregator = new PrecedentAggregator
                {
                    LastOccurrence = occurrenceDate,
                    Times = 1,
                    PrecedentValue = precedentValue
                };
                Elements.Add(precedentAggregator);
            }
            else
            {
                precedentAggregator.AddPrecedent(occurrenceDate);
            }
        }


        public bool IsSomeElements()
        {
            return Elements.Count > 0;
        }

        public T GetMostActual()
        {
            if (Elements.Count < 1) throw new Exception("No elements to get");
            var estimationTime = DateTime.Now;
            var elementToGet = Elements.FirstOrDefault();
            var maxEst = Estimate(elementToGet, estimationTime);
            foreach (var precedentAggregator in Elements)
            {
                var est = Estimate(precedentAggregator, estimationTime);
                if (maxEst < est)
                {
                    maxEst = est;
                    elementToGet = precedentAggregator;
                }
            }
            return elementToGet.PrecedentValue;
        }

        public T GetLatest()
        {
            var elementToGet = GetLatestAggregator();
            return elementToGet.PrecedentValue;
        }

        public PrecedentAggregator GetLatestAggregator()
        {
            if (Elements.Count < 1) return null;
            var estimationTime = DateTime.Now;
            var elementToGet = Elements.FirstOrDefault();
            var maxEst = EstimatePeriodOnly(elementToGet, estimationTime);
            foreach (var precedentAggregator in Elements)
            {
                var est = EstimatePeriodOnly(precedentAggregator, estimationTime);
                if (maxEst < est)
                {
                    maxEst = est;
                    elementToGet = precedentAggregator;
                }
            }
            return elementToGet;
        }


        public void ForgetElement()
        {
            if (Elements.Count < 1) throw new Exception("No elements to forget");
            var estimationTime = DateTime.Now;
            var elementToForget = Elements.FirstOrDefault();
            var minEstimation = Estimate(elementToForget, estimationTime);
            foreach (var precedentAggregator in Elements)
            {
                var est = Estimate(precedentAggregator, estimationTime);
                if (est < minEstimation)
                {
                    minEstimation = est;
                    elementToForget = precedentAggregator;
                }
            }
            Elements.Remove(elementToForget);
        }

        public void Clear()
        {
            Elements.Clear();
        }

        public double Estimate(PrecedentAggregator aggregator, DateTime estimationDate)
        {
            var abstractPeriod = (estimationDate - aggregator.LastOccurrence).Ticks/(double) (HalfLife.Ticks);
            double times = aggregator.Times;
            var est = times*Math.Pow(2, -abstractPeriod);
            return est;
        }

        public double EstimatePeriodOnly(PrecedentAggregator aggregator, DateTime estimationDate)
        {
            var abstractPeriod = (estimationDate - aggregator.LastOccurrence).Ticks/(double) (HalfLife.Ticks);
            var est = Math.Pow(2, -abstractPeriod);
            return est;
        }

        public List<T> GetArrangedList()
        {
            var list = GetArrangedAggregators();
            var returnList = new List<T>();
            foreach (var precedentAggregator in list)
            {
                returnList.Add(precedentAggregator.PrecedentValue);
            }
            return returnList;
        }

        public IOrderedEnumerable<PrecedentAggregator> GetArrangedAggregators()
        {
            var estimationTime = DateTime.Now;
            return Elements.OrderByDescending(((PrecedentAggregator pa) => Estimate(pa, estimationTime)));
        }


        public class PrecedentAggregator : INotifyPropertyChanged
        {
            private DateTime lastOccurrence;
            private T precedentValue;
            private int times;

            public T PrecedentValue
            {
                get { return precedentValue; }
                set
                {
                    precedentValue = value;
                    NotifyPropertyChange("Value");
                }
            }

            public DateTime LastOccurrence
            {
                get { return lastOccurrence; }
                set
                {
                    lastOccurrence = value;
                    NotifyPropertyChange("LastOccurrence");
                }
            }

            public int Times
            {
                get { return times; }
                set
                {
                    times = value;
                    NotifyPropertyChange("Times");
                }
            }

            [XmlIgnore]
            public string StringTimes
            {
                get { return string.Format(Times > 1 ? "{0} times" : "once", Times); }
                set { }
            }

            [XmlIgnore]
            public string StringAgo
            {
                get
                {
                    var timeSpan = DateTime.Now - LastOccurrence;
                    string mesureItem;
                    int valueItem;
                    if (timeSpan.TotalDays > 1)
                    {
                        var years = timeSpan.TotalDays/365;
                        if (years > 1)
                        {
                            valueItem = (int) years;
                            mesureItem = "year";
                        }
                        else
                        {
                            var months = timeSpan.TotalDays/30.41;
                            if (months > 1)
                            {
                                valueItem = (int) months;
                                mesureItem = "month";
                            }
                            else
                            {
                                valueItem = timeSpan.Days;
                                mesureItem = "day";
                            }
                        }
                    }
                    else
                    {
                        if (timeSpan.Hours > 0)
                        {
                            valueItem = timeSpan.Hours;
                            mesureItem = "hour";
                        }
                        else if (timeSpan.Minutes > 0)
                        {
                            valueItem = timeSpan.Minutes;
                            mesureItem = "minute";
                        }
                        else
                        {
                            return "just now";
                        }
                    }
                    return string.Format(valueItem > 1 ? "{0} {1}s ago" : "1 {1} ago", valueItem, mesureItem);
                }
                set { }
            }

            internal void AddPrecedent(DateTime occurrenceDate)
            {
                LastOccurrence = occurrenceDate;
                Times = Times + 1;
            }

            public void NotifyEx()
            {
                NotifyPropertyChange("PrecedentValue");
                NotifyPropertyChange("StringTimes");
            }

            #region INotifyPropertyChanged Members

            public event PropertyChangedEventHandler PropertyChanged;

            private void NotifyPropertyChange(string prop)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged.Invoke(this, new PropertyChangedEventArgs(prop));
                }
            }

            #endregion
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChange(string prop)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(prop));
            }
        }

        #endregion
    }
}